require 'cgi'

# Implements a filter dropdown that works much like Excel where all
# unique values are in the dropdown (plus a few special values). Select
# an option from the dropdown and the table will filter to show values
# that meet that criteria. See FilteredList::FilterScope for how to
# handle the controller side of things and FilteredList::Helper for
# how to handle the view site of things.
module FilteredList

  # Extensions to ActiveRecord for filtering
  module FilterScope
    # Pass in the filter param generated by the helper methods. By
    # default this will be named params[:filter]. Example:
    #
    #   Tasks.filter(params[:filter]).all
    def filter(filter)
      filter = filter.reject {|key, value| value == ':all'}
      filter.inject(scoped) {|scope, (field, value)| scope.where field => value}
    end
  end

  # Helper functions to generate filter selects
  module Helper

    # Generate a select for filtering.
    #
    # collection::
    #   The collection that we will iterate over to get our list of
    #   filter options.
    #
    # field::
    #   A method to be called on each object in the collection to
    #   generate an item in the select list.
    #
    # If just generating a simple select then all you need is to
    # provide the two arguments above. The option and block provide
    # further enhancements
    # 
    # For example if the filter is for a price you will want the value
    # submitted to be just a raw value but you want the option to
    # display to be some nicely formatted text. To do this just provide
    # a block that will get the values for itself and return either
    # a single value (for both value and label on the option) or a
    # two element array (first element is display, second element is
    # value). The following might be a price filter:
    #
    #   filter @orders, :price do |order|
    #     [order.formatted_price, order.raw_price]
    #   end
    #
    # Or perhaps your filter is a related object. Say you want to
    # provide a filter by customer:
    #
    #   filter @orders, :customer_id do |order|
    #     [order.customer.to_s, order.customer_id]
    #   end
    #
    # The options argument will allow you to control how the select
    # list is generated. The only supported option right now is :prefix
    # which will determine under what variable the filter is submitted.
    # The default value for this options is "filter".
    #
    # Finally you can pass a html_options hash which will be applied to
    # the select tag
    def filter(collection, field, options={}, html_options={}, &blk)
      options.reverse_merge! :prefix => 'filter'
      field = field.to_s
      cur = params[options[:prefix]][field]
      cur = '' if cur == ':all'

      blk = lambda {|obj| obj.send field.to_sym} if blk.nil?

      collection = collection.collect(&blk).collect do |value|
        label, value = value
        value = label if value.nil?
        [label.to_s.strip, value]
      end.uniq.reject {|v| v.first.blank?}.sort {|a, b| a.first <=> b.first}
      collection.unshift [cur, cur] unless
        cur.blank? || collection.find {|value| value.last == cur}
      collection.unshift ['All', ':all']
      collection = options_for_select collection, (params[options[:prefix]] || {})[field]

      html_options[:onchange] = "#{html_options[:onchange]};if($('#{escape_javascript options[:prefix]}_button')) $('#{escape_javascript options[:prefix]}_button').disabled = false;"
      html_options[:class] = "#{html_options[:class]} #{options[:prefix]}_select".strip
      html_options[:id] = "#{options[:prefix]}-#{field}"

      select_tag "#{options[:prefix]}[#{field}]", collection, html_options
    end

    # Creates a button that when clicked will reload the current page
    # with all existing params still applied but with the filter set to
    # the new filter values.
    #
    # If you use the :prefix option in the filter helper you must use
    # the same :prefix option in this helper to make the two work
    # together
    def filter_button(label='Apply Filter', options={}, html_options={})
      options.reverse_merge! :prefix => 'filter'
      html_options.reverse_merge! :disabled => true, :type => 'button',
        :value => label, :id => "#{options[:prefix]}_button"

      p = params.dup
      p.delete_if {|key, value| [options[:prefix], 'action', 'controller'].include? key}
      p = p.to_query
      p += '&' unless p == ''

      html_options[:onclick] = "#{html_options[:onclick]};location.href='?#{p}' + $$('select.#{escape_javascript options[:prefix]}_select').inject([], function(m, e) {m.push(e.getAttribute('name') + '=' + encodeURIComponent($F(e))); return m}).join('&')"

      tag 'input', html_options
    end
  end

  class Railtie < Rails::Railtie
    initializer 'filtered_list.integration' do
      ActiveRecord::Base.extend FilteredList::FilterScope
      ActionController::Base.helper FilteredList::Helper
    end
  end if defined? Rails
end

